ManyToManyRelation.java
package org.codefilarete.stalactite.engine.configurer.manytomany;
import java.util.Collection;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import org.codefilarete.reflection.Accessor;
import org.codefilarete.reflection.AccessorChain;
import org.codefilarete.reflection.AccessorChain.ValueInitializerOnNullValue;
import org.codefilarete.reflection.ReversibleAccessor;
import org.codefilarete.stalactite.dsl.PolymorphismPolicy;
import org.codefilarete.stalactite.dsl.entity.EntityMappingConfiguration;
import org.codefilarete.stalactite.dsl.entity.EntityMappingConfigurationProvider;
import org.codefilarete.stalactite.dsl.property.CascadeOptions.RelationMode;
import org.codefilarete.tool.Reflections;
import org.danekja.java.util.function.serializable.SerializableBiConsumer;
import org.danekja.java.util.function.serializable.SerializableFunction;
/**
*
* @param <SRC> the "one" type
* @param <TRGT> the "many" type
* @param <TRGTID> identifier type of TRGT
* @param <C1> the "many" collection type
*/
public class ManyToManyRelation<SRC, TRGT, TRGTID, C1 extends Collection<TRGT>, C2 extends Collection<SRC>> {
/** The method that gives the "many" entities from the "one" entity */
private final ReversibleAccessor<SRC, C1> collectionAccessor;
private final BooleanSupplier sourceTablePerClassPolymorphic;
/** Configuration used for "many" side beans persistence */
private final EntityMappingConfigurationProvider<TRGT, TRGTID> targetMappingConfiguration;
/** Default relation mode is {@link RelationMode#ALL} */
private RelationMode relationMode = RelationMode.ALL;
/** Optional provider of collection instance to be used if collection value is null */
private Supplier<C1> collectionFactory;
private final MappedByConfiguration<SRC, TRGT, C2> mappedByConfiguration;
/**
* Indicates that relation must be loaded in same main query (through join) or in some separate query
*/
private boolean fetchSeparately;
/**
* Indicates if target instances are indexed in their {@link Collection} (meaning that it is capable of storing order)
*/
private boolean ordered = false;
private String indexingColumnName;
/**
*
* @param collectionAccessor provider of the property to be persisted
* @param sourceTablePerClassPolymorphic indicates that source is table-per-class polymorphic
* @param targetMappingConfiguration persistence configuration provider of entities stored in the target collection
*/
public ManyToManyRelation(ReversibleAccessor<SRC, C1> collectionAccessor,
BooleanSupplier sourceTablePerClassPolymorphic,
EntityMappingConfigurationProvider<? super TRGT, TRGTID> targetMappingConfiguration) {
this.collectionAccessor = collectionAccessor;
this.sourceTablePerClassPolymorphic = sourceTablePerClassPolymorphic;
this.targetMappingConfiguration = (EntityMappingConfigurationProvider<TRGT, TRGTID>) targetMappingConfiguration;
this.mappedByConfiguration = new MappedByConfiguration<>();
}
public ManyToManyRelation(ReversibleAccessor<SRC, C1> collectionAccessor,
BooleanSupplier sourceTablePerClassPolymorphic,
EntityMappingConfigurationProvider<? super TRGT, TRGTID> targetMappingConfiguration,
MappedByConfiguration<?, TRGT, ?> mappedByConfiguration) {
this.collectionAccessor = collectionAccessor;
this.sourceTablePerClassPolymorphic = sourceTablePerClassPolymorphic;
this.targetMappingConfiguration = (EntityMappingConfigurationProvider<TRGT, TRGTID>) targetMappingConfiguration;
// Note that this cast is wrong, but left for simplicity: this constructor is used for embedded many-to-many relation, which means that the SRC
// type is the one that embed another one which contains the relation. In such configuration, the relation actually points to the embeddable
// type, not the one that embeds the relation. This the mappedBy(..) config does the same: it point to the embeddable type, not the SRC type.
// But fixing it has a lot of impacts due to the necessity to replace MappedByConfiguration "SRC" type by a generic <?> one with has its own
// complexity. I consider all these impacts doesn't worth it and I prefer to force this cast, even wrong.
this.mappedByConfiguration = (MappedByConfiguration<SRC, TRGT, C2>) mappedByConfiguration;
}
public ReversibleAccessor<SRC, C1> getCollectionAccessor() {
return collectionAccessor;
}
public boolean isSourceTablePerClassPolymorphic() {
return sourceTablePerClassPolymorphic.getAsBoolean();
}
/** @return the configuration used for "many" side beans persistence */
public EntityMappingConfiguration<TRGT, TRGTID> getTargetMappingConfiguration() {
return targetMappingConfiguration.getConfiguration();
}
public boolean isTargetTablePerClassPolymorphic() {
return getTargetMappingConfiguration().getPolymorphismPolicy() instanceof PolymorphismPolicy.TablePerClassPolymorphism;
}
public RelationMode getRelationMode() {
return relationMode;
}
public void setRelationMode(RelationMode relationMode) {
this.relationMode = relationMode;
}
@Nullable
public Supplier<C1> getCollectionFactory() {
return collectionFactory;
}
public void setCollectionFactory(Supplier<C1> collectionFactory) {
this.collectionFactory = collectionFactory;
}
public MappedByConfiguration<SRC, TRGT, C2> getMappedByConfiguration() {
return mappedByConfiguration;
}
@Nullable
public String getAssociationTableName() {
return this.mappedByConfiguration.getAssociationTableName();
}
public void setAssociationTableName(@Nullable String tableName) {
this.mappedByConfiguration.setAssociationTableName(tableName);
}
@Nullable
public String getSourceJoinColumnName() {
return this.mappedByConfiguration.getSourceJoinColumnName();
}
public void setSourceJoinColumnName(@Nullable String sourceJoinColumnName) {
this.mappedByConfiguration.setSourceJoinColumnName(sourceJoinColumnName);
}
@Nullable
public String getTargetJoinColumnName() {
return this.mappedByConfiguration.getTargetJoinColumnName();
}
public void setTargetJoinColumnName(@Nullable String targetJoinColumnName) {
this.mappedByConfiguration.setTargetJoinColumnName(targetJoinColumnName);
}
public void setIndexingColumnName(String columnName) {
ordered();
this.indexingColumnName = columnName;
}
@Nullable
public String getIndexingColumnName() {
return indexingColumnName;
}
public boolean isOrdered() {
return ordered;
}
public void setOrdered(boolean ordered) {
this.ordered = ordered;
}
public void ordered() {
this.ordered = true;
}
public boolean isFetchSeparately() {
return fetchSeparately;
}
public void setFetchSeparately(boolean fetchSeparately) {
this.fetchSeparately = fetchSeparately;
}
public void fetchSeparately() {
setFetchSeparately(true);
}
/**
* Clones this object to create a new one with the given accessor as prefix of current one.
* Made to shift current instance with an accessor prefix. Used for embeddable objects with relation to make the relation being accessible
* from the "root" entity.
*
* @param accessor the prefix of the clone to be created
* @return a clones of this instance prefixed with the given accessor
* @param <E> the root entity type that owns the embeddable which has this relation
*/
public <E, S extends Collection<E>> ManyToManyRelation<E, TRGT, TRGTID, C1, S> embedInto(Accessor<E, SRC> accessor, Class<SRC> embeddedType) {
AccessorChain<E, C1> shiftedTargetProvider = new AccessorChain<>(accessor, collectionAccessor);
shiftedTargetProvider.setNullValueHandler(new ValueInitializerOnNullValue() {
@Override
protected <T> T newInstance(Accessor<?, T> segmentAccessor, Class<T> valueType) {
if (segmentAccessor == accessor) {
return (T) Reflections.newInstance(embeddedType);
} else if (segmentAccessor == collectionAccessor){
if (collectionFactory != null) {
return (T) collectionFactory.get();
} else {
return super.newInstance(segmentAccessor, valueType);
}
} else {
return super.newInstance(segmentAccessor, valueType);
}
}
});
ShiftedMappedByConfiguration<E, SRC, TRGT, C2> shiftedMappedByConfiguration = this.mappedByConfiguration.embedInto(accessor);
ManyToManyRelation<E, TRGT, TRGTID, C1, S> result = new ManyToManyRelation<>(shiftedTargetProvider,
this::isSourceTablePerClassPolymorphic,
this.targetMappingConfiguration,
shiftedMappedByConfiguration);
result.setRelationMode(this.getRelationMode());
result.setFetchSeparately(this.isFetchSeparately());
result.setIndexingColumnName(this.getIndexingColumnName());
result.setOrdered(this.isOrdered());
result.setCollectionFactory(this.getCollectionFactory());
return result;
}
public static class MappedByConfiguration<SRC, TRGT, C2 extends Collection<SRC>> {
/**
* Combiner of target entity with source entity
*/
@Nullable
private SerializableBiConsumer<TRGT, SRC> reverseCombiner;
/**
* Source getter on target for bidirectionality (no consequence on database mapping).
*/
@Nullable
private SerializableFunction<TRGT, C2> reverseCollectionAccessor;
/**
* Source setter on target for bidirectionality (no consequence on database mapping).
*/
@Nullable
private SerializableBiConsumer<TRGT, C2> reverseCollectionMutator;
/** Optional provider of collection instance to be used if collection value is null */
@Nullable
private Supplier<C2> reverseCollectionFactory;
@Nullable
protected String associationTableName;
@Nullable
protected String sourceJoinColumnName;
@Nullable
protected String targetJoinColumnName;
@Nullable
public SerializableBiConsumer<TRGT, SRC> getReverseCombiner() {
return reverseCombiner;
}
public void setReverseCombiner(@Nullable SerializableBiConsumer<TRGT, SRC> reverseCombiner) {
this.reverseCombiner = reverseCombiner;
}
@Nullable
public SerializableFunction<TRGT, C2> getReverseCollectionAccessor() {
return reverseCollectionAccessor;
}
public void setReverseCollectionAccessor(@Nullable SerializableFunction<TRGT, C2> reverseCollectionAccessor) {
this.reverseCollectionAccessor = reverseCollectionAccessor;
}
@Nullable
public SerializableBiConsumer<TRGT, C2> getReverseCollectionMutator() {
return reverseCollectionMutator;
}
public void setReverseCollectionMutator(@Nullable SerializableBiConsumer<TRGT, C2> reverseCollectionMutator) {
this.reverseCollectionMutator = reverseCollectionMutator;
}
@Nullable
public Supplier<C2> getReverseCollectionFactory() {
return reverseCollectionFactory;
}
public void setReverseCollectionFactory(@Nullable Supplier<C2> reverseCollectionFactory) {
this.reverseCollectionFactory = reverseCollectionFactory;
}
@Nullable
public String getAssociationTableName() {
return associationTableName;
}
public void setAssociationTableName(@Nullable String associationTableName) {
this.associationTableName = associationTableName;
}
@Nullable
public String getSourceJoinColumnName() {
return sourceJoinColumnName;
}
public void setSourceJoinColumnName(@Nullable String sourceJoinColumnName) {
this.sourceJoinColumnName = sourceJoinColumnName;
}
@Nullable
public String getTargetJoinColumnName() {
return targetJoinColumnName;
}
public void setTargetJoinColumnName(@Nullable String targetJoinColumnName) {
this.targetJoinColumnName = targetJoinColumnName;
}
public boolean isEmpty() {
return reverseCollectionAccessor == null && reverseCollectionMutator == null && reverseCollectionFactory == null && reverseCombiner == null;
}
<C> ShiftedMappedByConfiguration<C, SRC, TRGT, C2> embedInto(Accessor<C, SRC> accessor) {
return new ShiftedMappedByConfiguration<>(accessor, this);
}
}
public static class ShiftedMappedByConfiguration<C, SRC, TRGT, C2 extends Collection<SRC>> extends MappedByConfiguration<SRC, TRGT, C2> {
private final Accessor<C, SRC> shifter;
private ShiftedMappedByConfiguration(Accessor<C, SRC> shifter, MappedByConfiguration<SRC, TRGT, C2> mappedByConfiguration) {
setReverseCombiner(mappedByConfiguration.getReverseCombiner());
setReverseCollectionAccessor(mappedByConfiguration.getReverseCollectionAccessor());
setReverseCollectionMutator(mappedByConfiguration.getReverseCollectionMutator());
setReverseCollectionFactory(mappedByConfiguration.getReverseCollectionFactory());
this.shifter = shifter;
}
public Accessor<C, SRC> getShifter() {
return shifter;
}
}
}